emoji selector
https://gyazo.com/d57fea8a143650375af1c8bba1fc1370
2018/9/25
コードブロックの頭とコードブロック中は動作しないようにupdate
/foldrr/foldrr.iconさんが作ったものを反映させていただいた
不満が残るところの一つだったので非常に嬉しい
ありがとうございます!
説明
:aaaa:を打つと絵文字(scrapbox上のアイコン)が入力できるようになるUserScript https://gyazo.com/03837650aa8c36fa33ab18903d539bb6
入力した文字に曖昧マッチングした候補が下に出てくる
上下キーを押して選択しEnterを押すか、クリックすると絵文字の文字列[something.icon]に置換できる
選択
https://gyazo.com/475a5e5c0f7158ee5d7891c743eb9e7e
クリック
https://gyazo.com/ce4818914d9044b1e85ac4a510111d71
:yutaro: => yutaro.icon
完全に入力すると、自動で置き換える
https://gyazo.com/1b92d7bb038eafe339c8c7ffa5170f1f
:yuta + Enter => yutaro.icon
入力途中でEnterを押すと一番上の候補が入力される
https://gyazo.com/75983960dfa47fbdc2e1a90497427cd5
右のemoji selector.iconボタンから load emojis from /emoji をクリックすると/emoji/にあるアイコンも使えるようになる (デフォルトではロードしたプロジェクト内の半角英数字, _ , 空白文字, "+-"で構成されているアイコンのみがページがデータセットとして読み込まれる)
:+1: => /emoji/+1.icon
:parrot: => /emoji/parrot.icon
余談
個人的に一番欲しい機能だったので作ってみた
同期的なコミュニケーションのキラーコンテンツは絵文字とリアクションだと思う
まとまった文章より伝えやすくて楽しい
jQueryあんまり書いたことないからいい感じにかけてるかどうか全くわからない...
全体的にかなり無理やり実装しているので、色々バグがあるかも...
参考にしたページ
検索にはここに書いてあるやつを雑に実装して使ってみた
ドロップダウンメニューは画面上部の検索窓とほぼ同じもの
cssとかをそのまま使っている
文字を置き換える部分で使っている
サンプルと解説記事がいっぱいあると組み合わせるだけで色々作れて楽しい!!
code:script.js
const Asearch = (function() {
var INITPAT, INITSTATE, MAXCHAR;
INITPAT = 0x80000000;
MAXCHAR = 0x100;
Asearch.prototype.isupper = function(c) {
return (c >= 0x41) && (c <= 0x5a);
};
Asearch.prototype.islower = function(c) {
return (c >= 0x61) && (c <= 0x7a);
};
Asearch.prototype.tolower = function(c) {
if (this.isupper(c)) {
return c + 0x20;
} else {
return c;
}
};
Asearch.prototype.toupper = function(c) {
if (this.islower(c)) {
return c - 0x20;
} else {
return c;
}
};
function Asearch(source) {
var c, i, j, len, mask, ref, ref1;
this.source = source;
this.shiftpat = [];
this.epsilon = 0;
this.acceptpat = 0;
mask = INITPAT;
for (c = i = 0, ref = MAXCHAR; 0 <= ref ? i < ref : i > ref; c = 0 <= ref ? ++i : --i) {
}
ref1 = this.unpack(this.source);
for (j = 0, len = ref1.length; j < len; j++) {
if (c === 0x20) {
this.epsilon |= mask;
} else {
mask >>>= 1;
}
}
this.acceptpat = mask;
return this;
}
Asearch.prototype.state = function(state, str) {
var c, i, i0, i1, i2, i3, len, mask, ref;
if (state == null) {
state = INITSTATE;
}
if (str == null) {
str = '';
}
ref = this.unpack(str);
for (i = 0, len = ref.length; i < len; i++) {
i3 = (i3 & this.epsilon) | ((i3 & mask) >>> 1) | (i2 >>> 1) | i2;
i2 = (i2 & this.epsilon) | ((i2 & mask) >>> 1) | (i1 >>> 1) | i1;
i1 = (i1 & this.epsilon) | ((i1 & mask) >>> 1) | (i0 >>> 1) | i0;
i0 = (i0 & this.epsilon) | ((i0 & mask) >>> 1);
i1 |= i0 >>> 1;
i2 |= i1 >>> 1;
i3 |= i2 >>> 1;
}
};
Asearch.prototype.match = function(str, ambig) {
var s;
if (ambig == null) {
ambig = 0;
}
s = this.state(INITSTATE, str);
if (!(ambig < INITSTATE.length)) {
ambig = INITSTATE.length - 1;
}
return (sambig & this.acceptpat) !== 0; };
Asearch.prototype.unpack = function(str) {
var bytes, c, code, i, len, ref;
bytes = [];
ref = str.split('');
for (i = 0, len = ref.length; i < len; i++) {
code = c.charCodeAt(0);
if (code > 0xFF) {
bytes.push((code & 0xFF00) >>> 8);
}
bytes.push(code & 0xFF);
}
return bytes;
};
return Asearch;
})();
const projectName = scrapbox.Project.name;
let emojis = [];
const box = $('<div>').addClass('form-group').css("position", "absolute");
const container = $('<div>').addClass('dropdown');
box.append(container);
let items = $('<ul>').addClass('dropdown-menu');
container.append(items);
$('#editor').append(box);
fetch(/api/pages/${projectName}?limit=10000, { credentials: 'same-origin'})
.then( res => res.text())
.then( text => {
const data = JSON.parse( text );
const pages = data.pages;
pages.filter( page => (page.image !== null && page.title.match(/^\w\s\-\++$/))) .forEach( page => {
emojis.push({
name: page.title,
path: page.title,
icon: /api/pages/${projectName}/${page.title}/icon,
})
})
})
scrapbox.PageMenu.addMenu({
title: 'emoji',
})
scrapbox.PageMenu('emoji').addItem({
title: "load emojis from /emoji",
onClick: () => {
fetch('/api/pages/emoji?limit=10000')
.then( res => res.text())
.then( text => {
const data = JSON.parse( text );
const pages = data.pages;
pages.filter( page => (page.image !== null && page.title.match(/^\w\s\-\++$/))) .forEach( page => {
for( let emoji of emojis ) {
if( emoji.name === page.title )return;
}
emojis.push({
name: page.title,
path: '/emoji/' + page.title,
icon: /api/pages/emoji/${page.title}/icon,
})
})
})
}
})
// TODO: 様々な文字列が来る場合を考慮する
const taberareloo = ( word, list ) => {
const targetWord = word.replace(':', '');
const regStr = targetWord.split('').reduce( (pre, cur) => pre + cur + '.*' ).replace('+', '\\+');
const reg = RegExp(regStr,'i');
return list.filter( item => item.name.match(reg));
}
const asearched = ( word, list ) => {
const targetWord = word.replace(':', '');
const a = new Asearch( targetWord );
const limitCount = Math.floor( targetWord.length/ 4 ) + 1;
let result = [];
for(let i = 0; i <= limitCount; i++){
let matched = list.filter( item => a.match( item.name, i));
let notExisted = matched.filter( item => {
for( let r of result){
if(r.name === item.name){
return false;
}
}
return true;
})
}
return result;
}
const fizzSearch = ( word, list ) => {
const a = asearched( word, list );
const b = taberareloo( word, list );
const c = b.filter( item => {
for( let r of a ){
if( r.name == item.name){
return false;
}
}
return true;
})
}
let stack = "";
const editor = $('#editor');
const open = () => container.addClass("open");
const close = () => {
stack = "";
container.removeClass("open");
}
const replaceText = (text, cursor, emojiPath) => {
cursor.focus();
setTimeout(()=>{
for(let i = 0; i < text.length; i++){
var ke1 = document.createEvent("Events");
ke1.initEvent("keydown", true, true);
ke1.keyCode = ke1.which = 8; // Backspace
cursor.dispatchEvent(ke1);
}
document.execCommand('insertText',null, [${emojiPath}.icon] );
close();
}, 50)
}
editor.keydown( e => {
const key = e.key;
if(key === undefined ) return;
if( stack === "" && key !== ":"){
close();
return
};
if ($('.cursor-line').text().trim() == 'code:'
|| $('.cursor-line .code-block').length == 1) {
close()
return;
}
if( key === ':' && stack.length !== 0){
let name = stack.replace(':', '');
for(let emoji of emojis){
if( emoji.name === name ){
let cursor = $('#text-input')0; replaceText(stack + ":", cursor, emoji.path);
return;
}
}
close()
return;
}
const cursor = $('#text-input')0; stack += e.key;
let focused = $(':focus');
if(focused.is(items.find('li > a'))){
cursor.focus();
}
}
if( stack.length === 2 ){
if( key === " " ){
stack = "";
return;
}
open();
}
switch(key){
case 'Backspace':
stack = stack.slice(0, stack.length - 1);
if(stack.length === 0){
close();
return;
}
break;
case 'ArrowUp':
let focusedUp = $(':focus');
if( focusedUp.is(items.find('li > a').eq(0)) ){
e.stopPropagation();
cursor.focus();
}else if( !focusedUp.is(items.find('li > a')) ){
close();
return;
}
break;
case 'ArrowDown':
let focusedDown = $(':focus');
if( !focusedDown.is(items.find('li > a'))) {
e.stopPropagation();
e.preventDefault();
items.find("li > a").eq(0).focus();
}
break;
case 'Escape':
case 'ArrowLeft':
case 'ArrowRight':
case 'Home':
case 'End':
case 'PageUp':
case 'PageDown':
close();
break;
case 'Enter':
if( stack.length === 1 ){
close();
break;
}
let focused = $(':focus');
if(!focused.is(items.find('li > a'))){
e.stopPropagation();
e.preventDefault();
items.find('li > a').eq(0).click();
}
break;
}
if( stack.length <= 1 || !key.match(/^\w\s\:\-\+$|Backspace/)) return; const matchedEmoji = fizzSearch(stack, emojis)
if( matchedEmoji.length === 0){
close();
return;
}
const newItems = $('<ul>').addClass('dropdown-menu');
matchedEmoji.forEach( ( emoji, index) => {
if( index > 30 ) return;
const li = $('<li>').addClass('dropdown-item');
const a = $('<a>').attr("tabindex", "0");
const img = $('<img>').attr("src", emoji.icon)
.addClass("icon").css({ height: "17px", float: "left"});
const nameTag = $('<div>').text(" :" + emoji.name + ":");
a.append(img);
a.append(nameTag);
li.append(a);
newItems.append(li);
a.on('click', () => {
cursor.focus();
replaceText(stack, cursor, emoji.path);
})
a.on('keypress', ev => {
if(ev.key === "Enter"){
ev.preventDefault();
ev.stopPropagation();
replaceText(stack, cursor, emoji.path);
}
})
})
items.replaceWith(newItems);
items = newItems;
let css = {};
cursor.style.cssText.split(';').filter( text => text !== '' )
.forEach( text => {
const props = text.split(':').map( text => text.replace(' ', '').replace('px', ''));
});
box.css({
top: ${parseInt(css.top) + parseInt(css.height) + 3}px,
left: ${css.left}px,
});
})